/*
 * Copyright 2019 WeBank
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.webank.wedatasphere.linkis.manager.engineplugin.manager.utils;

import com.webank.wedatasphere.linkis.manager.engineplugin.common.EngineConnPlugin;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.function.Function;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * Description:
 * Provide some methods to find classpath
 */
public class EngineConnPluginUtils {


    private static final Logger LOG = LoggerFactory.getLogger(EngineConnPluginUtils.class);


    private static final String JAR_SUF_NAME = ".jar";

    private static final String CLASS_SUF_NAME = ".class";

    public static final String FILE_SCHEMA = "file://";

    private static final Class<EngineConnPlugin> PLUGIN_PARENT_CLASS = EngineConnPlugin.class;


    public static List<URL> getJarsUrlsOfPath(String path){
        List<URL> classPathURLs = new ArrayList<>();
        return getJarsUrlsOfPathRecurse(path, classPathURLs);
    }

    public static String getEngineConnPluginClass(URLClassLoader engineClassloader){
        URL[] urlsOfClassLoader = engineClassloader.getURLs();
        String className;
        for(URL classPath : urlsOfClassLoader){
            String path = classPath.getPath();
            className = getEngineConnPluginClassFromURL(path, (classFullName) ->
                    isSubEngineConnPluginClass(classFullName, engineClassloader));
            if(null != className){
                return className;
            }
        }
        return null;
    }

    /**
     * Load EnginePlugin in spi( you should have constructor with no parameters)
     * @param classLoader engine plugin classloader
     * @return
     */
    public static EngineConnPlugin loadSubEngineConnPluginInSpi(ClassLoader classLoader){
        ServiceLoader<EngineConnPlugin> serviceLoader = ServiceLoader.load(PLUGIN_PARENT_CLASS, classLoader);
        Iterator<EngineConnPlugin> subClassIterator = serviceLoader.iterator();
        //Just return the first one
        if(subClassIterator.hasNext()){
            return subClassIterator.next();
        }
        return null;
    }

    private static List<URL> getJarsUrlsOfPathRecurse(String path, List<URL> classPathURLs){
        File parentFile = new File(path);
        File[] childFiles = parentFile.listFiles((file) ->{
          String name = file.getName();
          return !name.startsWith(".") && (file.isDirectory() || name.endsWith(JAR_SUF_NAME) ||name.endsWith(CLASS_SUF_NAME));
        });
        if(null != childFiles && childFiles.length > 0){
            for(File childFile : childFiles){
                if (childFile.isDirectory()) {
                    getJarsUrlsOfPathRecurse(childFile.getPath(), classPathURLs);
                } else {
                    try {
                        classPathURLs.add(childFile.toURI().toURL());
                    } catch (MalformedURLException e) {
                        //Ignore
                        LOG.warn("url {} cannot be added", FILE_SCHEMA + childFile.getPath());
                    }
                }
            }
        }
        return classPathURLs;
    }

    /**
     * Get engine plugin class
     * @param url url
     * @param acceptedFunction accept function
     * @return
     */
    private static String getEngineConnPluginClassFromURL(String url, Function<String, Boolean> acceptedFunction){
        if(url.endsWith(CLASS_SUF_NAME)){
            String className = url.substring(0, url.lastIndexOf(CLASS_SUF_NAME));
            int splitIndex = className.lastIndexOf(IOUtils.DIR_SEPARATOR);
            if(splitIndex >= 0){
                className = className.substring(splitIndex);
            }
            return acceptedFunction.apply(className) ? className : null;
        }else if(url.endsWith(JAR_SUF_NAME)){
            try {
                JarFile jarFile = new JarFile(new File(url));
                Enumeration<JarEntry> en = jarFile.entries();
                while(en.hasMoreElements()){
                    String name = en.nextElement().getName();
                    if(name.endsWith(CLASS_SUF_NAME)){
                        String className = name.substring(0, name.lastIndexOf(CLASS_SUF_NAME));
                        //If the splicer is different in WINDOWS system?
                        className = className.replaceAll(String.valueOf(IOUtils.DIR_SEPARATOR_UNIX), ".");
                        if(acceptedFunction.apply(className)){
                            return className;
                        }
                    }
                }
                return null;
            } catch (IOException e) {
                //Trace
                LOG.trace("Fail to parse jar file:[" + url + "] in plugin classpath");
                return null;
            }
        }
        return null;
    }

    private static boolean isSubEngineConnPluginClass(String className, ClassLoader classLoader) {
        if (StringUtils.isEmpty(className)) {
            return false;
        }
        Class<?> clazz;
        try {
            clazz = Class.forName(className, false, classLoader);
            //Skip interface and abstract class
            if (Modifier.isAbstract(clazz.getModifiers()) || Modifier.isInterface(clazz.getModifiers())) {
                return false;
            }
        } catch (Throwable t) {
            LOG.trace("Class: {} can not be found", className, t);
            return false;
        }
        return EngineConnPluginUtils.PLUGIN_PARENT_CLASS.isAssignableFrom(clazz);
    }


}
